iT邦幫忙

2024 iThome 鐵人賽

DAY 30
0

最後來打造「依測站名稱查詢」的功能。
關鍵字打上React Leaflet Search,有些外掛看起來是包含世界各地的經緯數據。比較廣泛,也不見得會有測站數據。
抓input裡的值去改StationName應該比較有效。

一不做二不休,但直接在index寫這樣不行。

<div id="root">
  <div id="search">
    <label>
    鄉鎮別:
    <input placeholder="台北市大安區 or 大安區" />
    <button>查詢</button>
    </label>
  </div>
</div>

那麼就再開一個Search組件。

熟悉的紅字最對味:Cannot read properties of null (reading 'value')。
直覺是先寫useEffect試試。
不過此時還須搭配useRef和useChange才行。
解決完null值問題,再讓input在發生變化時也可以取得最新資訊。

import { useState, useEffect, useRef } from "react";

export default function Search() {
  const [iv, setIv] = useState("");
  const ir = useRef(null);

  useEffect(() => {
    if (ir.current) {
      setIv(ir.current.value);
    }
  }, []);

  const handleChange = (e) => {
    setIv(e.target.value);
  };

  const handleSearch = () => {
    console.log(iv);
  };

  return (
    <div className="search">
      <label>
        站名:
        <input
          ref={ir}
          placeholder="e.g. 阿里山國小"
          onChange={handleChange}
        ></input>
        <button onClick={handleSearch}>查詢</button>
      </label>
    </div>
  );
}

接下來用傳遞props的概念,跨JS檔傳遞iv (input value)值。
再稍微整理一下程式碼之後變成這樣:

//Search.js
import "./styles.css";
import "leaflet/dist/leaflet.css";
import { useState, useEffect, useRef } from "react";

export default function Search({ onSearch }) {
  const [iv, setIv] = useState("");
  const ir = useRef(null);

  useEffect(() => {
    if (ir.current) {
      setIv(ir.current.value);
    }
  }, []);

  const handleChange = (e) => {
    setIv(e.target.value);
  };

  const handleSearch = () => {
    onSearch(iv);
  };

  return (
    <div className="search">
      <label>
        站名:
        <input
          ref={ir}
          placeholder="e.g. 阿里山國小"
          onChange={handleChange}
        ></input>
        <button onClick={handleSearch}>查詢</button>
      </label>
    </div>
  );
}
//App.js
import "./styles.css";
import "leaflet/dist/leaflet.css";
import {
  MapContainer,
  Marker,
  Popup,
  SVGOverlay,
  TileLayer,
  useMap,
} from "react-leaflet";
import { useState, useEffect } from "react";
import Search from "./Search";
import L from "leaflet";

L.Icon.Default.imagePath = "https://unpkg.com/leaflet/dist/images/";

function ChangeView({ position }) {
  const map = useMap();
  useEffect(() => {
    map.setView(position);
  }, [position, map]);
  return null;
}

export default function App() {
  const [name, setName] = useState("");
  const [rain, setRain] = useState(0);
  const [la, setLa] = useState(0);
  const [lo, setLo] = useState(0);
  const [bounds, setBounds] = useState([
    [0, 0],
    [0, 0],
  ]);
  const [searchName, setSearchName] = useState("嘉義");

  const position = [la, lo];

  const handleSearch = (value) => {
    setSearchName(value);
  };

  useEffect(() => {
    fetch(
      "https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0002-001?Authorization=你的API key"
    )
      .then((res) => res.json())
      .then(
        (resJson) =>
          resJson.records.Station.filter((s) => s.StationName === searchName)[0]
            .StationName
      )
      .then((resJson) => setName(resJson))
      .catch((err) => console.log(err));
  }, [searchName]);

  useEffect(() => {
    fetch(
      "https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0002-001?Authorization=你的API key"
    )
      .then((res) => res.json())
      .then(
        (resJson) =>
          resJson.records.Station.filter((s) => s.StationName === searchName)[0]
            .RainfallElement.Now.Precipitation
      )
      .then((resJson) => setRain(resJson))
      .catch((err) => console.log(err));
  }, [searchName]);

  useEffect(() => {
    fetch(
      "https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0002-001?Authorization=你的API key"
    )
      .then((res) => res.json())
      .then(
        (resJson) =>
          resJson.records.Station.filter((s) => s.StationName === searchName)[0]
            .GeoInfo.Coordinates[1].StationLatitude
      )
      .then((resJson) => setLa(resJson))
      .catch((err) => console.log(err));
  }, [searchName]);

  useEffect(() => {
    fetch(
      "https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0002-001?Authorization=你的API key"
    )
      .then((res) => res.json())
      .then(
        (resJson) =>
          resJson.records.Station.filter((s) => s.StationName === searchName)[0]
            .GeoInfo.Coordinates[1].StationLongitude
      )
      .then((resJson) => setLo(resJson))
      .catch((err) => console.log(err));
  }, [searchName]);

  useEffect(() => {
    fetch(
      "https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0002-001?Authorization=你的API key"
    )
      .then((res) => res.json())
      .then((resJson) => {
        const station = resJson.records.Station.find(
          (s) => s.StationName === searchName
        );
        const la = station.GeoInfo.Coordinates[1].StationLatitude;
        const lo = station.GeoInfo.Coordinates[1].StationLongitude;
        setBounds([
          [la, lo - 0.07],
          [la + 0.07, lo + 0.05],
        ]);
      })
      .catch((err) => console.log(err));
  }, [searchName]);

  return (
    <>
      <Search onSearch={handleSearch} />
      <MapContainer center={position} zoom={12} scrollWheelZoom={true}>
        <TileLayer
          attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
          url="http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png"
        />
        <Marker position={position}>
          <Popup>
            {name}, {rain}mm
          </Popup>
        </Marker>
        <ChangeView position={position} />
        {rain > 0 && (
          <SVGOverlay key={JSON.stringify(bounds)} bounds={bounds}>
            <defs>
              <symbol id="drop" viewBox="0 -10 80 10">
                <line stroke="#4ea6e9" strokeWidth="1%">
                  <animate
                    attributeName="x1"
                    from="30"
                    to="0"
                    dur="1s"
                    repeatCount="indefinite"
                  />
                  <animate
                    attributeName="y1"
                    from="0"
                    to="60"
                    dur="1s"
                    repeatCount="indefinite"
                  />
                  <animate
                    attributeName="x2"
                    from="30"
                    to="15"
                    dur="1s"
                    repeatCount="indefinite"
                  />
                  <animate
                    attributeName="y2"
                    from="0"
                    to="30"
                    dur="1s"
                    repeatCount="indefinite"
                  />
                </line>
              </symbol>
            </defs>
            <use xlinkHref="#drop" x="0" y="0" />
            <use xlinkHref="#drop" x="10%" y="0" />
            <use xlinkHref="#drop" x="20%" y="0" />
            <use xlinkHref="#drop" x="30%" y="0" />
            <use xlinkHref="#drop" x="40%" y="0" />
            <use xlinkHref="#drop" x="50%" y="0" />
            <use xlinkHref="#drop" x="60%" y="0" />
            <use xlinkHref="#drop" x="70%" y="0" />
            <use xlinkHref="#drop" x="80%" y="0" />
            <use xlinkHref="#drop" x="90%" y="0" />
          </SVGOverlay>
        )}
      </MapContainer>
    </>
  );
}

我們首先將iv包在onSearch()裡,然後放到Search的括號中,就像props。
這樣當使用者在App.js用到Search組件,就可以把躲在onSearch裡的iv值拿來用了。

另一邊,在APP.js裡設定searchName的state,記得不要粗心把初始值一併改成searchName,才不會報錯:Cannot access 'searchName' before initialization。

然後在每個useEffect的依賴項裡填入[searchName]。這樣每當使用者按下查詢引發handleSearch,導致searchName被重新set後,就會從API抓新的值。
並且把<Search>改成<Search onSearch={handleSearch} />,才能真正叫得動handleSearch。

測試時發現有個小缺陷是:如果兩間測站太近,地標icon會比圖資更迫不及待地跑走,哈。

Edit Leaflet2

最後的最後,我希望能為欄位加入「搜尋建議」功能。
而react-search-autocomplete正好能滿足所求。

基本上就是把所有測站名稱重新mapping成一個陣列。
在這個陣列裡有很多items。當使用者在選中搜尋建議時,則會重新得到items裡的iv (input value)值,也就是items裡被選中的那個測站名稱。再用onSearch和父組件App.js聯繫。

一開始搜尋框長得很怪,把原本search的CSS改掉就好了。
然而明明能正常傳出數值的查詢按鈕卻無法作用。
反倒是框內輸入正確時,只要按enter,視圖就會跳轉。

翻找一下文件,看來這個外掛本來就打算全包搜尋工作。
索性讓它克盡己職。整體效果也十分流暢,很滿意。

再把之前沒特別調的margin和padding歸零,實現真正的全螢幕後——
成功建好一張能搜尋某測站有沒有下雨的漂亮地圖,就是完賽的感人時刻了。

Edit Leaflet3



感謝終於沒有輕易言棄的自己。
也感謝點進來看我絮絮叨叨的你。
四年前剛認識開發人員工具。
四年後,若能再和旁人閒聊前端技藝,願我的眼神能更堅定。


上一篇
【Day29】React Leaflet 2
系列文
【現在學React還來得及嗎?】30天Takeaway分享30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言